package cool.mtc.io.upload;

import cool.mtc.core.result.Result;
import cool.mtc.core.result.ResultConstant;
import cool.mtc.core.util.StringUtil;
import cool.mtc.io.upload.exception.UploadConfigException;
import cool.mtc.io.upload.exception.UploadParamException;
import cool.mtc.io.upload.model.UploadInfo;
import cool.mtc.io.upload.model.UploadResult;
import cool.mtc.io.util.ResourceUtil;
import lombok.Cleanup;
import org.springframework.core.io.Resource;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.channels.FileChannel;
import java.util.*;

/**
 * @author 明河
 */
public class UploadTemplate {
    private final UploadProperties uploadProperties;

    public static final Map<String, UploadProperties.Type> UPLOAD_TYPE_MAP = new HashMap<>();
    public static final Map<String, UploadResult> UPLOAD_RESULT_MAP = new HashMap<>();

    public UploadTemplate(UploadProperties uploadProperties) {
        this.uploadProperties = uploadProperties;
        uploadProperties.getTypes().forEach(item -> UPLOAD_TYPE_MAP.put(item.getType(), item));
    }

    public void upload(UploadInfo uploadInfo) throws IOException {
        // 前置检查
        this.handleCheckUploadInfoCorrect(uploadInfo);

        // 如果是切片上传，记录上传结果，用以判断什么时候合并文件
        if (this.isChunkUpload(uploadInfo.getType())) {
            uploadInfo.setChunk(true);
            UploadResult result = UPLOAD_RESULT_MAP.get(uploadInfo.getChunkId());
            if (null == result) {
                UPLOAD_RESULT_MAP.put(uploadInfo.getChunkId(), new UploadResult(uploadInfo.getChunkTotal()));
            }
        }

        // 更新文件信息
        this.updateUploadInfo(uploadInfo);

        // 保存文件
        this.save(uploadInfo);
    }

    /**
     * 检查上传信息是否正确
     * <p>
     * 1. 文件类型是否符合要求，检查文件扩展名是否正确
     * 2. 文件大小是否符合要求
     */
    private void handleCheckUploadInfoCorrect(UploadInfo uploadInfo) {
        Optional.ofNullable(uploadInfo.getFile())
                .orElseThrow(() -> new UploadParamException("请选择上传的文件"));
        Optional.ofNullable(uploadInfo.getType())
                .orElseThrow(() -> new UploadParamException("请选择上传的类型"));
        Optional.ofNullable(UPLOAD_TYPE_MAP.get(uploadInfo.getType()))
                .orElseThrow(() -> new UploadConfigException(ResultConstant.ERROR.newInstance().msg("上传的类型[{0}]未配置").args(uploadInfo.getType())));

        UploadProperties.Type type = UPLOAD_TYPE_MAP.get(uploadInfo.getType());

        // 文件类型是否符合要求
        List<String> allowExtensionList = type.getAllowExtensions();
        if (!allowExtensionList.contains("*")) {
            String fileName = StringUtil.ifEmpty(uploadInfo.getFile().getOriginalFilename(), uploadInfo.getFile().getName());
            String extension = fileName.contains(StringUtil.POINT) ? fileName.substring(fileName.lastIndexOf(StringUtil.POINT) + 1) : StringUtil.EMPTY;
            if (allowExtensionList.stream().noneMatch(item -> item.equals(extension))) {
                throw new UploadParamException(ResultConstant.ERROR.newInstance().msg("所选文件的格式[{0}]不符合要求").args(extension));
            }
        }

        // 文件大小是否符合要求
        if (this.isChunkUpload(uploadInfo.getType())) {
            // 如果启用了切片上传，检查切片参数是否正确
            if (StringUtil.isEmpty(uploadInfo.getChunkId())) {
                throw new UploadParamException("上传文件的切片ID未设置");
            }
            Optional.ofNullable(uploadInfo.getChunkTotal())
                    .orElseThrow(() -> new UploadParamException("上传文件的切片总数未设置"));
            Optional.ofNullable(uploadInfo.getChunkTotal())
                    .orElseThrow(() -> new UploadParamException("上传文件的切片顺序未设置"));

            // 切片上传，要求最后一片需要小于等于设置的切片大小，其他片需等于设置的切片大小
            if (uploadInfo.getChunkTotal().equals(uploadInfo.getChunkIndex())) {
                if (uploadInfo.getFile().getSize() > type.getInuseChunk(uploadProperties).getSize().toBytes()) {
                    throw new UploadParamException("文件切片大小不符合要求");
                }
            } else {
                if (!(uploadInfo.getFile().getSize() == type.getInuseChunk(uploadProperties).getSize().toBytes())) {
                    throw new UploadParamException("文件切片大小不符合要求");
                }
            }
        } else {
            if (uploadInfo.getFile().getSize() > type.getMaxSize().toBytes()) {
                Result<Object> result = ResultConstant.ERROR
                        .newInstance()
                        .msg("文件大小[{0}]超出限制[{1}]")
                        .args(uploadInfo.getFile().getSize(), type.getMaxSize().toBytes());
                throw new UploadParamException(result);
            }
        }

    }

    /**
     * 更新上传的文件信息
     */
    private void updateUploadInfo(UploadInfo uploadInfo) throws IOException {
        MultipartFile file = uploadInfo.getFile();
        // 从原始文件获取原文件名
        String originalFileName = StringUtil.ifEmpty(file.getOriginalFilename(), file.getName());
        uploadInfo.setOriginalName(originalFileName);
        if (originalFileName.contains(StringUtil.POINT)) {
            // 获取文件扩展名
            uploadInfo.setExtension(originalFileName.substring(originalFileName.lastIndexOf(StringUtil.POINT) + 1));
        }
        // 新文件名
        if (StringUtil.isEmpty(uploadInfo.getName())) {
            // 以UUID为新文件名
            uploadInfo.setName(UUID.randomUUID() + StringUtil.POINT + uploadInfo.getExtension());
        } else {
            // 正确设置文件名.扩展名
            if (StringUtil.isNotEmpty(uploadInfo.getExtension()) && !uploadInfo.getName().endsWith(uploadInfo.getExtension())) {
                uploadInfo.setName(uploadInfo.getName() + StringUtil.POINT + uploadInfo.getExtension());
            }
        }
        uploadInfo.setMime(file.getContentType());
        if (!uploadInfo.isChunk()) {
            uploadInfo.setMd5(ResourceUtil.md5(file.getResource()));
            uploadInfo.setSha256(ResourceUtil.sha256(file.getResource()));
        }
        this.updateUploadFilePath(uploadInfo);
    }

    /**
     * 更新文件保存路径信息
     */
    private void updateUploadFilePath(UploadInfo uploadInfo) {
        UploadProperties.Type type = UPLOAD_TYPE_MAP.get(uploadInfo.getType());
        // 如果是切片上传，要把切片存放路径取出来
        if (uploadInfo.isChunk()) {
            UploadProperties.Chunk chunk = type.getInuseChunk(uploadProperties);
            String chunkBasePath = chunk.getInusePath(uploadProperties);
            // 文件名，命名规则：类型名/chunkId/文件名-<切片总数>_<切片顺序>
            String chunkRelativePath = uploadInfo.getType() + StringUtil.SLASH +
                    uploadInfo.getChunkId() + StringUtil.SLASH +
                    uploadInfo.getName() + StringUtil.UNDERLINE +
                    uploadInfo.getChunkTotal() + StringUtil.HYPHEN +
                    uploadInfo.getChunkIndex();
            uploadInfo.setChunkPath(chunkBasePath + chunkRelativePath);
        } else {
            uploadInfo.setRelativePath(type.isUseUuidFolder() ? ResourceUtil.plusUUIDFolder(uploadInfo.getName()) : uploadInfo.getName());
            uploadInfo.setAbsolutePath(type.getInusePath(uploadProperties) + uploadInfo.getRelativePath());
        }
    }

    /**
     * 保存上传的文件到指定位置
     */
    private void save(UploadInfo uploadInfo) throws IOException {
        @Cleanup InputStream inputStream = uploadInfo.getFile().getInputStream();
        // 如果是切片上传，要临时存到切片存放路径
        String targetFilePath = uploadInfo.isChunk() ? uploadInfo.getChunkPath() : uploadInfo.getAbsolutePath();
        @Cleanup OutputStream outputStream = this.getTargetFileOutputStream(targetFilePath);
        FileCopyUtils.copy(inputStream, outputStream);

        // 如果是切片上传，每次上传完成后检查切片数是否已完成
        if (uploadInfo.isChunk()) {
            UploadResult result = UPLOAD_RESULT_MAP.get(uploadInfo.getChunkId());
            result.setCompleteNum(result.getCompleteNum() + 1);

            if (result.getTotal() == result.getCompleteNum()) {
                this.merge(uploadInfo);
            }
        } else {
            uploadInfo.setComplete(true);
        }
    }

    /**
     * 合并切片文件到指定路径
     */
    private void merge(UploadInfo uploadInfo) throws IOException {
        if (!uploadInfo.isChunk()) {
            return;
        }

        // 切片所在文件夹
        File chunkFile = ResourceUtil.getResource(uploadInfo.getChunkPath()).getFile();
        if (!chunkFile.exists()) {
            return;
        }
        File chunkFolder = chunkFile.getParentFile();
        File[] files = chunkFolder.listFiles();

        // 当前文件上传情况
        UploadResult result = UPLOAD_RESULT_MAP.get(uploadInfo.getChunkId());
        if (null == files || files.length == 0 || files.length != uploadInfo.getChunkTotal() || result.isComplete()) {
            return;
        }

        UploadProperties.Type type = UPLOAD_TYPE_MAP.get(uploadInfo.getType());
        uploadInfo.setRelativePath(type.isUseUuidFolder() ? ResourceUtil.plusUUIDFolder(uploadInfo.getName()) : uploadInfo.getName());
        uploadInfo.setAbsolutePath(type.getInusePath(uploadProperties) + uploadInfo.getRelativePath());

        FileChannel outChannel = this.getTargetFileOutputStream(uploadInfo.getAbsolutePath()).getChannel();
        for (File file : files) {
            FileChannel inChannel = new FileInputStream(file).getChannel();
            inChannel.transferTo(0, inChannel.size(), outChannel);
            inChannel.close();
            // 删除切片文件
            if (type.getInuseChunk(uploadProperties).isDeleteAfterComplete()) {
                file.delete();
            }
        }
        outChannel.close();

        // 删除切片文件夹
        chunkFolder.delete();

        // 合并后更新文件的MD5值
        uploadInfo.setMd5(ResourceUtil.md5(ResourceUtil.getResource(uploadInfo.getAbsolutePath())));
        uploadInfo.setSha256(ResourceUtil.md5(ResourceUtil.getResource(uploadInfo.getAbsolutePath())));

        // 更新完成标识
        uploadInfo.setComplete(true);
        result.setComplete(true);

        // 从Map中移除当前ID
        UPLOAD_RESULT_MAP.remove(uploadInfo.getChunkId());
    }

    /**
     * 获取生成文件的输出流
     */
    private FileOutputStream getTargetFileOutputStream(String targetFilePath) throws IOException {
        Resource resource = ResourceUtil.createResource(targetFilePath);
        return new FileOutputStream(resource.getFile().getAbsoluteFile());
    }

    /**
     * 是否切片上传
     */
    private boolean isChunkUpload(String uploadType) {
        UploadProperties.Type type = UPLOAD_TYPE_MAP.get(uploadType);
        return null != type && null != type.getInuseChunk(uploadProperties) && type.getInuseChunk(uploadProperties).isEnabled();
    }
}
